/*
 * Copyright (c) 2011 Imre Fazekas.
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * Neither the name of the Brillien nor the names of its
 * terms and concepts may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.streamline.services.io.configuration;

import com.vii.streamline.services.StringServices;
import org.w3c.dom.*;
import org.xml.sax.InputSource;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * ConfigurationReader's aim is to help handling XML-based tree-like structure. Using the standard XML handling libraries, a lot of woodworking, additional efforts arise resulting a big programming overhead.
 * This class is to help the XML processing flow
 */
public class ConfigurationReader {

    /**
     * The root element of the XML structure
     */
    protected ConfigurationTreeNode result;

    /**
     * Actual XML element being processed
     */
    protected ConfigurationTreeNode actual;

    /**
     * Getter method for field result
     */
    public ConfigurationTreeNode getResult() {
        return result;
    }

    private static HashMap<String, String> getAttributes( Element e ){
        NamedNodeMap m = e.getAttributes();
        HashMap<String, String> attributes = new HashMap<String, String>();
        for( int i=0; i<m.getLength(); ++i ){
            Node a = m.item(i);
            attributes.put( a.getNodeName(), a.getNodeValue() );
        }
        return attributes;
    }

    private void buildConfigurationTree( Node n ){
        if( n instanceof Element){
            actual = actual.addChild( ((Element)n).getTagName(), getAttributes( (Element)n ) );
            NodeList list = n.getChildNodes();
            for( int i=0; i<list.getLength(); ++i )
                buildConfigurationTree( list.item( i ) );
            actual = actual.getParent();
        }
        else if( n instanceof Text){
            String s = n.getNodeValue().trim();
            if( s.length() > 0 && ( Character.isLetterOrDigit( s.charAt(0) ) || s.charAt(0) == '-' || s.charAt(0) == '/' ) )
            actual.addChild( n.getNodeValue(), new HashMap<String, String>() );
        }
    }
    
    private ConfigurationTreeNode getTreeNode( ConfigurationTreeNode root, String... path ){
        ConfigurationTreeNode temp = root;
        for( int i=0; temp!=null && i<path.length; ++i ){
            temp = temp.getChild( path[i] );
        }

        if( temp == null )
            throw new IllegalArgumentException("Invalid path:: " + StringServices.stringify(path) );

        return temp;
    }

    private ConfigurationTreeNode getTreeNodeByParameter( ConfigurationTreeNode root, String parameterName, String parameterValue, String... path ){
        ConfigurationTreeNode temp = root;
        for( int i=0; temp!=null && i<path.length; ++i ){
            temp = temp.getChild( path[i], parameterName, parameterValue );
        }

        if( temp == null )
            throw new IllegalArgumentException("Invalid path:: " + StringServices.stringify(path) );
        
        return temp;
    }

    private void getTreeNodesByNodeNameRecursive( String nodeName, ConfigurationTreeNode root, List<ConfigurationTreeNode> nodes ){
        List<ConfigurationTreeNode> children = root.getChildren();
        for (ConfigurationTreeNode configurationTreeNode : children) {
            if( configurationTreeNode.getNodeValue().equals( nodeName ) )
                nodes.add( configurationTreeNode );
            getTreeNodesByNodeNameRecursive( nodeName, configurationTreeNode, nodes );
        }
    }

    private static ConfigurationReader innerReadConfig( ConfigurationReader reader, Document d ) throws Exception{
        Element e = d.getDocumentElement();
        reader.actual = reader.result = new ConfigurationTreeNode(
                e.getTagName(), null, getAttributes( e ) 
        );
        NodeList list = e.getChildNodes();
        for( int i=0; i<list.getLength(); ++i )
            reader.buildConfigurationTree( list.item( i ) );

        return reader;
    }

    /**
     * Service method to create new ConfigurationReader instance for an inpustream
     * @param in string to be processed
     * @return ConfigurationReader instance
     * @throws Exception
     */
    public static ConfigurationReader readConfig( String in ) throws Exception{
        ConfigurationReader reader = new ConfigurationReader();

        DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
        DocumentBuilder bd = f.newDocumentBuilder();
        Document d = bd.parse( new InputSource( new StringReader( in ) ) );

        return innerReadConfig( reader, d );
    }


    /**
     * Service method to create new ConfigurationReader instance for an inpustream
     * @param in inputstream to be processed
     * @return ConfigurationReader instance
     * @throws Exception
     */
    public static ConfigurationReader readConfig( InputStream in ) throws Exception{
        ConfigurationReader reader = new ConfigurationReader();

        DocumentBuilderFactory f = DocumentBuilderFactory.newInstance();
        DocumentBuilder bd = f.newDocumentBuilder();
        Document d = bd.parse( in );

        return innerReadConfig( reader, d );
    }

    /**
     * This method searches a node from a "root" node by a given path, and collects all children of that node by name
     * @param nodeName the name of the nodes to be collected
     * @param root root point of the search
     * @param path path of the target parent from the root
     * @return array of nodes
     */
    public ConfigurationTreeNode[] getTreeNodesByNodeName( String nodeName, ConfigurationTreeNode root, String... path ){
        List<ConfigurationTreeNode> nodes = new LinkedList<ConfigurationTreeNode>();

        ConfigurationTreeNode temp = getTreeNode( root, path );
        List<ConfigurationTreeNode> children = temp.getChildren();
        for (ConfigurationTreeNode configurationTreeNode : children) {
            //System.out.println(":::::" + configurationTreeNode + " " + configurationTreeNode.getNodeValue() + " " + nodeName + " " + configurationTreeNode.getNodeValue().equals( nodeName ) );
            if( configurationTreeNode.getNodeValue().equals( nodeName ) )
                nodes.add( configurationTreeNode );
        }

        //System.out.println("" + nodes.size() );
        return nodes.toArray( new ConfigurationTreeNode[ nodes.size() ] );
    }

    /**
     * Get All nodes with the given name from the tree
     * @param nodeName name of the node
     * @return array of the found nodes
     */
    public ConfigurationTreeNode[] getTreeNodesByNodeNameRecursive( String nodeName ){
        List<ConfigurationTreeNode> nodes = new LinkedList<ConfigurationTreeNode>();
        getTreeNodesByNodeNameRecursive( nodeName, result, nodes );
        return nodes.toArray( new ConfigurationTreeNode[ nodes.size() ] );
    }

    /**
     * This method searches a node from a "root" node by a given path, and collects all children of that node by nam
     * @param nodeName the name of the nodes to be collected
     * @param path path of the target parent from the root
     * @return array of nodes
     */
    public ConfigurationTreeNode[] getTreeNodesByNodeName( String nodeName, String... path ){
        return getTreeNodesByNodeName( nodeName, result, path );
    }

    /**
     * Get the contained values of a node at the end of the given path, as an array of strings
     * In an XML if a node contains multiple lines of data, it will be retrieved as separated String objects
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return string values of the node
     */
    public String[] getStringValues( ConfigurationTreeNode root, String... path ){
        ConfigurationTreeNode temp = getTreeNode( root, path );
        List<ConfigurationTreeNode> nodes = temp.getContainedValues();
        String[] values = new String[ nodes.size() ];
        for( int i=0; i<values.length; ++i )
            values[i] = nodes.get( 0 ).getNodeValue();
        return values;
    }

    /**
     * Get the first value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return string value
     */
    public String getStringValue( ConfigurationTreeNode root, String... path ){
        ConfigurationTreeNode temp = getTreeNode( root, path );
        return temp.getContainedValue().getNodeValue();
    }

    /**
     * Get the first value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param parameterName name of the parameter of the node
     * @param parameterValue value of the parameter of the node
     * @param path path to the node we are interested in
     * @return string value
     */
    public String getStringValueByParameter( ConfigurationTreeNode root, String parameterName, String parameterValue, String... path ){
        ConfigurationTreeNode temp = getTreeNodeByParameter( root, parameterName, parameterValue, path );
        return temp.getContainedValue().getNodeValue();
    }

    /**
     * Get the first value of a node, found by the given path starting from the given root
     * The "null" value will be replaced by null reference
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return string value
     */
    public String getStringValueHandlingNull( ConfigurationTreeNode root, String... path ){
        String s = getStringValue( root, path );
        return s.equals("null") ? null : s;
    }

    /**
     * Tells whether the path contains value or not
     */
    public boolean hasValueByParameter( String parameterName, String parameterValue, String... path ){
        ConfigurationTreeNode node = getTreeNode( result, path );
        if( node != null ){
            return node.hasAttribute(parameterName) && node.getAttribute(parameterName).equalsIgnoreCase(parameterValue);
        }
        return false;
    }

    /**
     * Tells whether the path contains value or not
     */
    public boolean hasValue( String... path ){
        return hasValue( result, path );
    }

    /**
     * Tells whether the path relative from the given root contains value or not
     */
    public boolean hasValue( ConfigurationTreeNode root, String... path ){
        try{
            return getTreeNode( root, path ) != null;
        } catch(Exception e){ }
        return false;
    }

    /**
     * Get the Byte value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Byte
     */    
    public Byte getByteValue( ConfigurationTreeNode root, String... path ){
        return new Byte( getStringValue( root, path ) );
    }

    /**
     * Get the Integer value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as an Integer
     */
    public Integer getIntegerValue( ConfigurationTreeNode root, String... path ){
        return new Integer( getStringValue( root, path ) );
    }

    /**
     * Get the Float value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Float
     */
    public Float getFloatValue( ConfigurationTreeNode root, String... path ){
        return new Float( getStringValue( root, path ) );
    }

    /**
     * Get the Double value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Double
     */
    public Double getDoubleValue( ConfigurationTreeNode root, String... path ){
        return new Double( getStringValue( root, path ) );
    }

    /**
     * Get the Boolean value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Boolean
     */
    public Boolean getBooleanValue( ConfigurationTreeNode root, String... path ){
        return Boolean.valueOf( getStringValue(root, path) );
    }

    /**
     * Get the Long value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Long
     */
    public Long getLongValue( ConfigurationTreeNode root, String... path ){
        return new Long( getStringValue( root, path ) );
    }

    /**
     * Get the Date value of a node, found by the given path starting from the given root
     * @param root the "root" of the information retrieving
     * @param path path to the node we are interested in
     * @return node value as a Date
     */
    public Date getDateValue( ConfigurationTreeNode root, String... path ){
        return new Date( getLongValue(root, path) );
    }

    /**
     * Get the String value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a String
     */
    public String getStringValue( String... path ){
        return getStringValue( result, path );
    }

    /**
     * Get the String value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @param parameterName name of the parameter of the node
     * @param parameterValue value of the parameter of the node
     * @return node value as a String
     */
    public String getStringValueByParameter( String parameterName, String parameterValue, String... path ){
        return getStringValueByParameter( result, parameterName, parameterValue, path );
    }

    /**
     * Get the String value of a node, found by the given path starting from the root
     * The "null" value will be replaced by null reference
     * @param path path to the node we are interested in
     * @return node value as a String
     */
    public String getStringValueHandlingNull( String... path ){
        String s = getStringValue( path );
        return s.equals("null") ? null : s;
    }

    /**
     * Get the Byte value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Byte
     */
    public Byte getByteValue( String... path ){
        return new Byte( getStringValue( path ) );
    }

    /**
     * Get the Integer value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as an Integer
     */
    public Integer getIntegerValue( String... path ){
        return new Integer( getStringValue( path ) );
    }

    /**
     * Get the Float value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Float
     */
    public Float getFloatValue( String... path ){
        return new Float( getStringValue( path ) );
    }

    /**
     * Get the Double value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Double
     */
    public Double getDoubleValue( String... path ){
        return new Double( getStringValue( path ) );
    }

    /**
     * Get the Boolean value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Boolean
     */
    public Boolean getBooleanValue( String... path ){
        return Boolean.valueOf(getStringValue(path));
    }

    /**
     * Get the Long value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Long
     */
    public Long getLongValue( String... path ){
        return new Long( getStringValue( path ) );
    }

    /**
     * Get the Date value of a node, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return node value as a Date
     */
    public Date getDateValue( String... path ){
        return new Date( getLongValue(path) );
    }

    /**
     * Get the String values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a String array
     */
    public String[] getStringValue( ConfigurationTreeNode[] nodes, String path ){
        String[] res = new String[ nodes.length ];
        for( int i=0; i<nodes.length; ++i )
            res[i] = getStringValue( nodes[i], path );
        return res;
    }

    /**
     * Get the Byte values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Byte array
     */
    public Byte[] getByteValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Byte[] res = new Byte[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = new Byte( sres[i] );
        return res;
    }

    /**
     * Get the Integer values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as an Integer array
     */
    public Integer[] getIntegerValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Integer[] res = new Integer[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = new Integer( sres[i] );
        return res;
    }

    /**
     * Get the Float values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Float array
     */
    public Float[] getFloatValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Float[] res = new Float[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = new Float( sres[i] );
        return res;
    }

    /**
     * Get the Double values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Double array
     */
    public Double[] getDoubleValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Double[] res = new Double[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = new Double( sres[i] );
        return res;
    }

    /**
     * Get the Boolean values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Boolean array
     */
    public Boolean[] getBooleanValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Boolean[] res = new Boolean[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = Boolean.valueOf( sres[i] );
        return res;
    }

    /**
     * Get the Long values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Long array
     */
    public Long[] getLongValue( ConfigurationTreeNode[] nodes, String path ){
        String[] sres = getStringValue( nodes, path );
        Long[] res = new Long[ sres.length ];
        for( int i=0; i<sres.length; ++i )
            res[i] = new Long( sres[i] );
        return res;
    }

    /**
     * Get the Date values of the nodes, found by the given path starting from the root
     * @param path path to the node we are interested in
     * @return nodes' values as a Date array
     */
    public Date[] getDateValue( ConfigurationTreeNode[] nodes, String path ){
        Long[] lres = getLongValue( nodes, path );
        Date[] res = new Date[ lres.length ];
        for( int i=0; i<lres.length; ++i )
            res[i] = new Date( lres[i] );
        return res;
    }
    

}
